Skip to content

Map [MinLength]/[MaxLength] on dictionary properties to minProperties / maxProperties#3922

Open
KitKeen wants to merge 5 commits intodomaindrivendev:masterfrom
KitKeen:fix/minlength-dictionary-minproperties
Open

Map [MinLength]/[MaxLength] on dictionary properties to minProperties / maxProperties#3922
KitKeen wants to merge 5 commits intodomaindrivendev:masterfrom
KitKeen:fix/minlength-dictionary-minproperties

Conversation

@KitKeen
Copy link
Copy Markdown

@KitKeen KitKeen commented Apr 24, 2026

Fixes #3915.

Problem

Applying [MinLength(1)] to a property typed as a dictionary (IDictionary<,>, IReadOnlyDictionary<,>, etc.) produces "minLength": 1 on the generated schema:

public class MyModel
{
    [Required]
    [MinLength(1)]
    public IReadOnlyDictionary<string, string?> Values { get; init; }
}

"minLength" on an object schema is invalid per the OpenAPI 3.x spec — it's defined only for string schemas. Linters such as vacuum flag this (the original reporter hit it exactly that way). The equivalent constraint for object schemas is "minProperties".

Fix

OpenApiSchemaExtensions.Apply{Min,Max,}LengthAttribute already branches on schema.Type to pick MinItems / MaxItems for arrays. This PR adds a parallel branch for JsonSchemaTypes.Object so dictionary schemas get MinProperties / MaxProperties instead of the meaningless MinLength / MaxLength.

No changes to the MinLengthRouteConstraint / MaxLengthRouteConstraint handlers — route parameters are primitives and cannot be dictionaries, so the existing Array/otherwise split is correct there.

Before / after

public class MyModel
{
    [Required]
    [MinLength(1)]
    public IReadOnlyDictionary<string, string?> Values { get; init; }
}

Before:

"MyModel": {
  "type": "object",
  "required": [ "values" ],
  "properties": {
    "values": {
      "type": "object",
      "additionalProperties": { "type": "string", "nullable": true },
      "minLength": 1
    }
  }
}

After:

"MyModel": {
  "type": "object",
  "required": [ "values" ],
  "properties": {
    "values": {
      "type": "object",
      "additionalProperties": { "type": "string", "nullable": true },
      "minProperties": 1
    }
  }
}

Tests

Added to OpenApiSchemaExtensionsTests:

  • MinLength on dictionary schema → minProperties
  • MaxLength on dictionary schema → maxProperties
  • Length(min,max) on dictionary schema → min+maxProperties
  • Regression guards: MinLength on string schema still maps to minLength; on array schema still maps to minItems.

Scope

  • No public API changes.
  • No dependency changes.
  • Behavior change is limited to schemas whose Type includes Object — previously those would receive MinLength/MaxLength, which was invalid per the OpenAPI spec, so any consumer relying on the old output was already producing non-conforming documents.

Checklist

  • Bug fix includes a test that fails without the fix.
  • No public API surface change.
  • Builds & tests pass locally. (Local SDK is 8.0; repo requires 10.0 — relying on CI.)

KitKeen added 2 commits April 24, 2026 09:25
…erties / maxProperties

Fixes domaindrivendev#3915. OpenAPI defines minLength/maxLength only for string schemas;
for object schemas (including dictionaries) the equivalent constraints
are minProperties/maxProperties. Before this change, applying
[MinLength(1)] to a property typed as IReadOnlyDictionary<TKey, TValue>
produced "minLength": 1 on the schema, which is invalid per the spec and
is flagged by linters such as vacuum.

The three attribute handlers (MinLength, MaxLength, Length) already
switch to MinItems/MaxItems when the schema is an Array. Extend the same
branching to recognise Object schemas and emit MinProperties/MaxProperties
instead of MinLength/MaxLength.

Route constraint handlers (MinLengthRouteConstraint, MaxLengthRouteConstraint)
are intentionally left alone: route parameters are primitives and cannot
be dictionaries, so the existing two-branch array/string logic is correct
there.

Tests added in OpenApiSchemaExtensionsTests:
- MinLength on dictionary → minProperties
- MaxLength on dictionary → maxProperties
- Length on dictionary → min+maxProperties
- Regression guards: MinLength on string still maps to minLength, on array still maps to minItems.
Added:
- End-to-end coverage via shared TypeWithValidationAttributes fixture —
  DictionaryWithMinMaxLength ([MinLength(1),MaxLength(3)]) and
  DictionaryWithLength ([Length(1,3)]) properties, asserted by both
  JsonSerializerSchemaGenerator and NewtonsoftSchemaGenerator tests.
  Reproduces the exact scenario from the issue (dictionary property on a
  model class) rather than only exercising ApplyValidationAttributes in
  isolation.
- Enum-keyed dictionary shape — Object schema with known Properties and
  AdditionalPropertiesAllowed=false (see CreateDictionarySchema). MinLength
  must still route to MinProperties here.
- Nullable dictionary shape — Type = Object | Null. HasFlag(Object) must
  still route to MinProperties.
- Symmetry regression guards: MaxLength/Length on string map to
  MinLength/MaxLength, on array map to MinItems/MaxItems (previously only
  MinLength had explicit guards).

Fixture updates are picked up by both Newtonsoft and JsonSerializer
generator test suites because the shared TestSupport fixture drives both.
Copy link
Copy Markdown
Collaborator

@martincostello martincostello left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make some test changes to that the OpenAPI snapshots demonstrate the required changes to the generated OpenAPI schemas.

[Fact]
public static void ApplyValidationAttributes_MinLength_On_Dictionary_Maps_To_MinProperties()
{
// Arrange — dictionary schema is represented as an Object with AdditionalProperties
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove all the AI s and replace with -s.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, removed all AI markers


public string[] ArrayWithMinMaxLength { get; set; }

public IReadOnlyDictionary<string, string> DictionaryWithMinMaxLength { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name doesn't seem to match the purpose?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed


public string[] ArrayWithLength { get; set; }

public Dictionary<string, string> DictionaryWithLength { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name doesn't seem to match the purpose?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

KitKeen added 2 commits April 25, 2026 11:12
- Replace em-dashes with hyphens in OpenApiSchemaExtensionsTests.cs
- Rename fixture property DictionaryWithMinMaxLength to
  IReadOnlyDictionaryWithMinMaxLength so the name matches its actual
  IReadOnlyDictionary<,> declared type
- Add JSON-output test that serializes the schema for
  TypeWithValidationAttributes/TypeWithValidationAttributesViaMetadataType
  to OpenAPI 3.0 JSON and asserts the dictionary properties surface as
  minProperties/maxProperties in the produced OpenAPI document
…ttribute

Per review: 'Length' as a noun does not describe a dictionary (which has a
property/item count, not a length). The Attribute suffix makes it explicit
that the property exists to test the [Length] attribute mapping rather than
a conceptual 'length' of the dictionary.

public string[] ArrayWithLength { get; set; }

public Dictionary<string, string> DictionaryWithLengthAttribute { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it doesn't have an attribute...


public string[] ArrayWithMinMaxLength { get; set; }

public IReadOnlyDictionary<string, string> IReadOnlyDictionaryWithMinMaxLength { get; set; }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there's no min or max.

Per follow-up review: in TypeWithValidationAttributesViaMetadataType the
outer class is a bare property list (attributes live on the inner
MetadataType class), so names that referenced attributes were misleading.

Renamed to neutral, type-focused names that work in both fixture variants:
- IReadOnlyDictionaryWithMinMaxLength -> BoundedReadOnlyDictionary
- DictionaryWithLengthAttribute       -> BoundedDictionary

The bounded prefix describes the test purpose (a dictionary whose size is
constrained); the readonly/mutable distinction reflects the actual type
under test. No attribute presence is claimed by the property name.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MinLength attribute on a dictionary property generates invalid minLength constraint

2 participants